[Previous] [Next]

Inheritance

After encapsulation and polymorphism, inheritance is the third major characteristic of all mature object-oriented programming languages. In Chapter 6, I briefly described what inheritance is and how it could be useful to programmers. And I also told you that—unfortunately—inheritance isn't natively supported by Visual Basic. In this section, I explain what you can do to remedy this deficiency.

Back to the Shapes sample program. This time, you'll write a CSquare class module, which adds support for drawing squares. Because this class is so similar to CRectangle, this could actually be a one-minute job: Just copy the CRectangle code into the CSquare module, and edit it where appropriate. For example, because a square is nothing but a rectangle with width equal to height, you could make both the Width and Height properties point to the same private variable.

This solution is somewhat unsatisfactory, however, because we have duplicated the code in the CRectangle class. If we later discover that the CRectangle class includes a bug, we must remember to correct it in the CSquare module, as well as in all other classes that were derived from CRectangle in the meantime. If Visual Basic supported true inheritance, we could just declare that the CSquare class inherits all its properties and methods from CRectangle, and then we could focus only on the few differences. Alas, this isn't possible, at least with the current version of Visual Basic. (I am an irrepressibly optimistic guy) On the other hand, the concept of inheritance is so alluring and promising that you might take a second look at it. As I'll show shortly, you can resort to a coding technique that lets you simulate inheritance at the expense of some manual coding.

Inheritance Through Delegation

The technique of simulating inheritance is called delegation. The concept is simple: because most of the logic needed in CSquare (the derived class) is embodied in CRectangle (the base class), the code in CSquare can simply ask a CRectangle object to do the work on its behalf.

Basic delegation techniques

So you do this trick by declaring a private CRectangle object inside the CSquare class and passing it all the calls that CSquare doesn't want to deal with directly. These calls include all methods and all read/write operations for properties. Here's a possible implementation of this technique:

' The CSquare Class
' This is the Private instance of the CRectangle class.
Private Rect As CRectangle

Private Sub Class_Initialize()
    ' Create the private variable for doing the delegation.
    Set Rect = New CRectangle
End Sub

' A simple pseudoconstructor for ease of use
Friend Sub Init(Left As Single, Top As Single, Width As Single, _
    Optional Color As Variant, Optional FillColor As Variant)
    ...
End Sub

' The delegation code
Property Get Left() As Single
    Left = Rect.Left
End Property
Property Let Left(ByVal newValue As Single)
    Rect.Left = newValue
End Property

Property Get Top() As Single
    Top = Rect.Top
End Property
Property Let Top(ByVal newValue As Single)
    Rect.Top = newValue
End Property

Property Get Width() As Single
    Width = Rect.Width
End Property
Property Let Width(ByVal newValue As Single)
    ' Squares are rectangles whose Width = Height.
    Rect.Width = newValue
    Rect.Height = newValue
End Property

Property Get Color() As Long
    Color = Rect.Color
End Property
Property Let Color(ByVal newValue As Long)
    Rect.Color = newValue
End Property

Property Get FillColor() As Long
    FillColor = Rect.FillColor
End Property
Property Let FillColor(ByVal newValue As Long)
    Rect.FillColor = newValue
End Property

Admittedly, it's a lot of code for such a simple task, but you shouldn't forget that we're playing with toy objects here. In a real program, the base class might include hundreds or thousands of lines of code. In that case, the relatively few lines needed for the delegation would be absolutely negligible.

Support for secondary interfaces

While our CSquare class is functional, it still doesn't know how to redraw itself. If the CRectangle class exposed the Draw, Move, and Zoom methods in its primary interface—as it did in the first version of the Shapes program—this would have been child's play. Unfortunately, we moved the Draw method from the CRectangle main interface to its IShape secondary interface. For this reason, in order to delegate this method we first need to get a reference to that interface:

' In the CSquare class
Private Sub IShape_Draw(pic As Object)
    Dim RectShape As IShape
    Set RectShape = Rect        ' Retrieve the IShape interface.
    RectShape.Draw pic          ' Now it works!
End Sub

Since you'll need a reference to Rect's IShape interface many times during the life of the CSquare class, you can speed up execution and reduce the amount of code by creating a module-level RectShape variable:

' CSquare also supports the IShape interface. 
Implements IShape

' This is the private instance of the CRectangle class.
Private Rect As CRectangle
' This points the Rect's IShape interface.
Private RectShape As IShape

Private Sub Class_Initialize()
    ' Create the two variables for doing the delegation.
    Set Rect = New CRectangle
    Set RectShape = Rect
End Sub
' ... code for Left, Top, Width, Color, FillColor properties ...(omitted)

' The IShape interface
Private Sub IShape_Draw(pic As Object)
    RectShape.Draw pic
End Sub

Private Property Let IShape_Hidden(ByVal RHS As Boolean)
    RectShape.Hidden = RHS
End Property
Private Property Get IShape_Hidden() As Boolean
    IShape_Hidden = RectShape.Hidden
End Property

Private Sub IShape_Move(stepX As Single, stepY As Single)
    RectShape.Move stepX, stepY
End Sub

Private Sub IShape_Zoom(ZoomFactor As Single)
    RectShape.Zoom ZoomFactor
End Sub

Subclassing the base class

While inheritance through delegation could be easily disregarded as a hack by any serious OO programmer working with mature OOPLs, the fact that you're in complete control of what happens during execution has several advantages. For example, when the client invokes a method in your derived class, you have several choices:

In the last two cases, your code is sometimes said to be subclassing the base class. It uses the base class for what can be useful but also executes some pre- and postprocessing code that adds power to the derived class. Even if the concept is vaguely similar, don't confuse it with control or Windows subclassing, which is a completely different (and more advanced) programming technique that lets you modify the behavior of standard Windows controls. (This type of subclassing is described in the Appendix.)

Subclassing the VBA language

You might not be aware that VBA gives you the means to subclass itself. As you know, Visual Basic can be considered the sum of the Visual Basic library and the VBA language. These libraries are always present in the References dialog box and can't be removed as other external libraries can. Even if you can't remove them, however, as far as the Visual Basic parser is concerned, the names that you use in your own code have a higher priority than the names defined in external libraries, including the VBA library! To see what I mean, add this simple procedure in a standard BAS module:

' An IIf replacement that accepts just one argument 
' If FalsePart is omitted and the expression is False, it returns Empty.
Function IIf(Expression As Boolean, TruePart As Variant, _
    Optional FalsePart As Variant) As Variant
    If Expression Then
        IIf = TruePart
    ElseIf Not IsMissing(FalsePart) Then
        IIf = FalsePart
    End If
End Function

You can call native VBA statements even if you're currently subclassing them, provided that you specify the name of the VBA library:

Function Hex(Value As Long, Optional Digits As Variant) As String
    If IsMissing(Digits) Then
        Hex = VBA.Hex(Value)
    Else
        Hex = Right$(String$(Digits, "0") & VBA.Hex(Value), Digits)
    End If
End Function

You should always try to keep the syntax of your new custom function compatible with that of the original VBA function so that you won't break any existing code.

One word of caution: This technique could give rise to problems, especially if you work on a team of programmers and not all of them are familiar with it. You can cope with this issue in part by always enforcing a compatible syntax, but this doesn't solve the problem when it falls to your colleagues to maintain or revise your code. For this reason, always consider the opportunity to define a new function with a different name and syntax so that your code isn't unnecessarily ambiguous.

Inheritance and Polymorphism

If you completely inherit a class module from another class—that is, you implement all the methods of the base class into the derived class—you end up with two modules that are very similar to one another, often to the point that you can use an Object variable to leverage their polymorphism and simplify your client code. On the other hand, you know that you don't need to resort to late binding (that is, Object variables) to get all the advantages of polymorphism because secondary interfaces always offer a much better alternative.

Implementing the base class as an interface

As an illustration of this concept, the CSquare class could implement the CRectangle interface:

' In the CSquare class module
Implements IShape
Implements CRectangle

' The primary and the IShape interface are identical... (omitted).... 
' This is the secondary CRectangle interface.

Private Property Let CRectangle_Color(ByVal RHS As Long)
    Rect.Color = RHS
End Property
Private Property Get CRectangle_Color() As Long
    CRectangle_Color = Rect.Color
End Property

Private Property Let CRectangle_FillColor(ByVal RHS As Long)
    Rect.FillColor = RHS
End Property
Private Property Get CRectangle_FillColor() As Long
    CRectangle_FillColor = Rect.FillColor
End Property

' The rect's Height property is replaced by the Width property.
Private Property Let CRectangle_Height(ByVal RHS As Single)
    rect.Width = RHS
End Property
Private Property Get CRectangle_Height() As Single
    CRectangle_Height = rect.Width
End Property

Private Property Let CRectangle_Left(ByVal RHS As Single)
    Rect.Left = RHS
End Property
Private Property Get CRectangle_Left() As Single
    CRectangle_Left = Rect.Left
End Property

Private Property Let CRectangle_Top(ByVal RHS As Single)
    Rect.Top = RHS
End Property
Private Property Get CRectangle_Top() As Single
    CRectangle_Top = Rect.Top
End Property

Private Property Let CRectangle_Width(ByVal RHS As Single)
    Rect.Width = RHS
End Property
Private Property Get CRectangle_Width() As Single
    CRectangle_Width = Rect.Width
End Property

In the CRectangle interface, you're using the same delegation technique that you saw before, so actually this isn't much of a shift in the organization of the class module. The benefits of this approach, however, are visible in the client application, which can now refer to either a CRectangle or a CSquare object using a single variable and through early binding:

Dim figures As New Collection
Dim rect As CRectangle, Top As Single

' Create a collection of rectangles and squares.
figures.Add New_CRectangle(1000, 2000, 1500, 1200)
figures.Add New_CSquare(1000, 2000, 1800)
figures.Add New_CRectangle(1000, 2000, 1500, 1500)
figures.Add New_CSquare(1000, 2000, 1100)

' Fill them, and stack them one over the other using early binding!
For Each rect In figures
    rect.FillColor = vbRed
    rect.Left = 0: rect.Top = Top
    Top = Top + rect.Height
Next

Add executable code to abstract classes

When I introduced abstract classes as a means of defining interfaces, I said that abstract classes never contain executable code, but only the definition of the interface. But the previous example shows that it's perfectly legal to use the same class module as an interface blueprint for an Implements statement and at the same time use the code inside it.

The CRectangle class is a rather complex application of this technique because it works as a regular class, as a base class from which you can inherit, and as an interface that you can implement in other classes. When you begin to be acquainted with objects, this approach will become natural.

The Benefits of Inheritance

Inheritance is a great OOP technique that lets programmers derive new classes with minimum effort. Simulation of true inheritance through delegation is the next best thing, and even if it takes some coding effort you should always consider it when you're creating several classes that are similar to one another because inheritance lets you reuse code and logic, enforce a better encapsulation, and ease code maintenance: